iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0

https://ithelp.ithome.com.tw/upload/images/20250917/20124462KA2M7PfuNm.png

Rust 逼我成為更好的工程師:從所有權到 API 設計

在前面幾篇中,我們深入理解了 Rust 的所有權系統:移動 (Move)、借用 (Borrow)、Copy 和 Clone。

今天把這些概念應用到實際的 API 設計中,看看如何寫出既安全又優雅的函式。

API 設計:函式簽名的哲學

https://ithelp.ithome.com.tw/upload/images/20250920/20124462zBVN6PBVRM.png

理解了所有權系統後,我們可以設計出更優雅的 API。函式簽名是技術規格,更是設計意圖的體現。

糟糕的 API 設計:不必要的所有權轉移

// 糟糕的設計:只是想計算長度,卻奪走了 String 的所有權
fn calculate_length(s: String) -> usize {
    s.len()
}

fn main() {
    let my_data = String::from("hello");
    let length = calculate_length(my_data); // my_data 在此被移動
    println!("長度是: {}", length);
    // println!("{}", my_data);  // ❌ 編譯錯誤!my_data 已失效
}

優秀的 API 設計:借用而非佔有

// 好的設計:函式簽名明確表達「我只讀取資料,不會拿走它」
fn calculate_length(s: &str) -> usize {
    s.len()
}

fn main() {
    let my_data = String::from("hello");
    let length = calculate_length(&my_data);  // 傳遞借用
    println!("原始資料: {}", my_data);   // ✅ 仍然有效
    println!("長度是: {}", length);
}

為什麼 &str&String 更好?

因為 &str 是一種字串切片 (string slice),它更加通用:

fn process_text(text: &str) {
    println!("處理: {}", text);
}

fn main() {
    let owned_string = String::from("hello");
    let string_literal = "world";
    let string_slice = &owned_string[0..3];
    
    // 一個函式,三種用法
    process_text(&owned_string);  // 借用 String
    process_text(string_literal); // 字串字面值
    process_text(string_slice);   // 字串切片
}

技術補充:為什麼 &String 能傳給需要 &str 的函式?

簡單來說,String 類型有一個「自動轉換」的能力。當你有一個 &String,但函式需要 &str 時,Rust 編譯器會自動幫你轉換。

這背後的原理是 String 實現了一個特殊的 trait,Deref
String 為了能被當作 &str 使用,實現了 Deref<Target=str>
告訴編譯器:「如果你需要 &str,但手上只有 &String,你可以把我當作 &str 來使用。」這樣我們的 API 就能同時接受字串字面值 ("hello") 和 String 的借用 (&my_string),非常方便!

不同類型的 API 設計模式

1. 讀取型 API:使用借用

// 好的設計:只讀取資料
fn get_length(s: &str) -> usize {
    s.len()
}

fn is_empty(s: &str) -> bool {
    s.is_empty()
}

fn contains(s: &str, pattern: &str) -> bool {
    s.contains(pattern)
}

2. 修改型 API:使用可變借用

// 好的設計:修改現有資料
fn append_text(s: &mut String, text: &str) {
    s.push_str(text);
}

fn clear_string(s: &mut String) {
    s.clear();
}

fn reverse_string(s: &mut String) {
    *s = s.chars().rev().collect();
}

3. 轉換型 API:取得所有權並回傳新值

// 好的設計:轉換資料並回傳新值
fn to_uppercase(s: String) -> String {
    s.to_uppercase()
}

fn add_prefix(s: String, prefix: &str) -> String {
    format!("{}{}", prefix, s)
}

fn reverse(s: String) -> String {
    s.chars().rev().collect()
}

4. 混合型 API:彈性的參數設計

// 好的設計:支援多種輸入類型
fn process_text<T: AsRef<str>>(text: T) -> String {
    let s = text.as_ref();
    s.to_uppercase()
}

fn main() {
    let owned = String::from("hello");
    let literal = "world";
    
    // 兩種用法都支援
    println!("{}", process_text(&owned));  // 借用
    println!("{}", process_text(owned));   // 移動
    println!("{}", process_text(literal)); // 字串字面值
}

總結:API 設計的智慧

通過理解所有權系統在 API 設計中的應用,我們看到了 Rust 的設計哲學:

  1. 明確性 (Explicitness):函式簽名清晰表達意圖
    • T:我要所有權
    • &T:我只讀取
    • &mut T:我要修改

如果函式只需要讀取資料,就用 &str 去「借」。
這是最常用、也最推薦的方式。

如果函式需要修改資料,就用 &mut String 去「借來改」。
記得變數本身也要宣告成 mut

如果函式是要消耗掉資料來創造新東西,或者是要把資料的所有權轉移走,才用 String 把東西「拿走」。

  1. 安全性 (Safety):編譯期就根除記憶體問題

    • 無懸空引用
    • 無資料競爭
    • 無記憶體洩漏
  2. 效率 (Efficiency):零成本抽象

    • 借用避免複製
    • 編譯器優化
    • 明確的資源管理
  3. 可預測性 (Predictability):資源生命週期明確

    • 何時創建
    • 何時銷毀
    • 誰擁有它

對習慣了「隱式共享」或「垃圾回收」的開發者來說,這套系統可能看起來過於嚴格。

但正是這種「先緊後鬆」的哲學,才能夠編寫出既安全又高效的程式碼。
在下一篇我們會深入探討生命週期 (Lifetimes),理解 Rust 如何確保「借用 Borrowing 」的時間安全性。

相關連結與參考資源


上一篇
(Day5) Rust Copy 與 Clone 零成本 vs 有成本複製
系列文
Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言